MySQL StatefulSet 部署方案

Posted by Forgus on 2026-04-12

部署背景

当前 MySQL 使用 Deployment 部署在 k3s-master 节点,存在以下问题:

  • Deployment 不适合有状态服务
  • 固定调度在 master 节点,应分配到 worker 节点
  • 缺乏稳定的网络标识

目标:使用 StatefulSet + Longhorn 重新部署,支持持久化存储和动态调度。


当前环境

项目
K3s 版本 v1.34.5+k3s1
存储 Longhorn (RWO)
网络插件 Flannel (10.42.0.0/16)
节点 6 节点 (1 amd64 + 5 arm64)

多层备份策略

为确保数据安全,采用 3 层备份:

层 1: SQL 逻辑备份

1
2
kubectl exec -n mysql mysql-55bfb88477-fd486 -- \
mysqldump -u root -p'{password}' gitea > /mnt/nfs/mysql-gitea-backup.sql
  • 位置:/mnt/nfs/mysql-gitea-backup.sql
  • 大小:约几百 KB
  • 优点:可读性好,恢复简单

层 2: 数据文件物理备份

1
2
3
kubectl exec -n mysql mysql-55bfb88477-fd486 -- \
tar -czf /tmp/mysql-data.tar.gz -C /var/lib/mysql . \
&& kubectl cp -n mysql mysql-55bfb88477-fd486:/tmp/mysql-data.tar.gz /mnt/nfs/mysql-data.tar.gz
  • 位置:/mnt/nfs/mysql-data.tar.gz
  • 大小:约几百 MB
  • 优点:包含所有表、索引、配置

层 3: 保留原 PVC 和 Deployment

  • 不删除原有资源,等新 StatefulSet 验证通过后再清理

关键注意事项

1. 镜像版本

使用固定版本而非浮动标签,避免小版本迭代导致兼容性问题:

1
image: mysql:8.0.36

2. MySQL 初始化时序

数据恢复必须等待 MySQL 完全初始化完成,否则会导致恢复失败。

3. Gitea 连接

当前 Gitea 使用 ClusterIP Service(mysql.mysql.svc.cluster.local:3306),迁移后 Service 不变,无需修改应用配置。


部署步骤

Step 0: 校验 Secret(前置检查)

1
2
3
4
5
6
7
# 验证 Secret 存在且包含必要字段
kubectl get secret mysql-secret -n mysql -o yaml

# 解码验证关键字段
kubectl get secret mysql-secret -n mysql -o jsonpath='{.data.MYSQL_ROOT_PASSWORD}' | base64 -d
kubectl get secret mysql-secret -n mysql -o jsonpath='{.data.MYSQL_DATABASE}' | base64 -d
kubectl get secret mysql-secret -n mysql -o jsonpath='{.data.MYSQL_USER}' | base64 -d
  • 必需字段:MYSQL_ROOT_PASSWORDMYSQL_DATABASEMYSQL_USERMYSQL_PASSWORD
  • 如缺失或错误,需提前修复

Step 1: 验证原 MySQL 正常

1
kubectl exec -n mysql mysql-55bfb88477-fd486 -- mysqladmin ping -u root -p'{password}'

Step 2: 执行 3 层备份

  • 层 1:SQL 导出到 /mnt/nfs/mysql-gitea-backup.sql
  • 层 2:数据文件到 /mnt/nfs/mysql-data.tar.gz
  • 验证备份文件存在

Step 3: 创建 StatefulSet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
apiVersion: v1
kind: Service
metadata:
name: mysql-headless
namespace: mysql
spec:
clusterIP: None
ports:
- port: 3306
name: mysql
selector:
app: mysql
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
namespace: mysql
spec:
serviceName: mysql-headless
replicas: 1
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
preference:
matchExpressions:
- key: node-role.kubernetes.io/worker
operator: Exists
containers:
- name: mysql
image: mysql:8.0.36
envFrom:
- secretRef:
name: mysql-secret
env:
- name: TZ
value: Asia/Shanghai
volumeMounts:
- name: data
mountPath: /var/lib/mysql
- name: mysql-log
mountPath: /var/log/mysql
volumes:
- name: mysql-log
emptyDir: {}
livenessProbe:
exec:
command:
- mysqladmin
- ping
- -h
- 127.0.0.1
initialDelaySeconds: 60
periodSeconds: 30
readinessProbe:
exec:
command:
- mysqladmin
- ping
- -h
- 127.0.0.1
initialDelaySeconds: 30
periodSeconds: 10
resources:
requests:
cpu: 200m
memory: 512Mi
limits:
cpu: 1000m
memory: 1Gi
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes:
- ReadWriteOnce
storageClassName: longhorn
resources:
requests:
storage: 20Gi
  • 特点:
    • 使用 VolumeClaimTemplate 自动创建新 PVC
    • 节点亲和性:优先调度到 worker 节点
    • 健康检查:liveness + readiness probes

Step 4: 创建 NodePort Service

1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: v1
kind: Service
metadata:
name: mysql
namespace: mysql
spec:
type: NodePort
ports:
- port: 3306
nodePort: 30306
name: mysql
selector:
app: mysql
  • 端口:30306
  • 局域网可访问:192.168.2.x:30306

Step 5: 等待 MySQL 初始化完成

1
2
3
4
5
# 等待 Pod 进入 Running 状态
kubectl wait --for=condition=ready pod/mysql-0 -n mysql --timeout=300s

# 验证 MySQL 可接受连接(初始化完成标志)
kubectl exec -n mysql mysql-0 -- mysqladmin ping -u root -p'{password}'

Step 6: 恢复数据到新 Pod

1
2
3
4
5
# 从备份文件恢复
cat /mnt/nfs/mysql-gitea-backup.sql | kubectl exec -i mysql-0 -n mysql -- mysql -u root -p'{password}' gitea

# 验证数据恢复成功
kubectl exec -n mysql mysql-0 -- mysql -u root -p'{password}' gitea -e "SHOW TABLES;"

Step 7: 验证

  • 检查 Pod 状态:kubectl get pod mysql-0 -n mysql -o wide
  • 检查服务:kubectl get svc mysql -n mysql
  • 测试连接:mysql -h 192.168.2.40 -P 30306 -u root -p'{password}'
  • 验证 gitea 应用连接:检查应用日志无错误

Step 8: 稳定运行观察

1
2
3
# 建议观察 24 小时后再清理旧资源
# 监视新 Pod 运行状态
watch -n 10 'kubectl get pod mysql-0 -n mysql -o wide'

Step 9: 清理旧资源

1
2
# 确认新服务稳定运行后(至少 24 小时),删除旧资源
kubectl delete deployment mysql -n mysql

验证清单

立即验证(迁移后)

验证项 方式
Pod 运行状态 kubectl get pod mysql-0 -n mysql
MySQL 初始化完成 mysqladmin ping 成功
数据恢复成功 SHOW TABLES; 有表
局域网访问 mysql -h 192.168.2.x -P 30306 -u root -p

延迟验证(24 小时后)

验证项 方式
Pod 运行稳定 24 小时内无重启
gitea 连接正常 检查 gitea Pod 日志无连接错误

回退方案

如有问题,执行:

1
2
3
4
5
# 停止新 StatefulSet
kubectl delete statefulset mysql -n mysql

# 恢复 Deployment(旧数据仍保留在原 PVC)
kubectl apply -f /mnt/nfs/mysql-deployment.yaml

问题修复记录

1. Readiness Probe 报错

问题:readiness probe 失败,报错 Can't connect to local MySQL server through socket '/var/run/mysqld/mysqld.sock'

原因:探针使用 -h localhost(socket 连接),容器启动时 socket 文件未就绪

修复:将探针改为 TCP 连接

1
2
3
4
5
6
7
# 旧
- -h
- localhost

# 新
- -h
- 127.0.0.1

2. 局域网客户端连接报错

问题:Sequel Pro 客户端连接报错 Authentication plugin 'caching_sha2_password' cannot be loaded

原因:MySQL 8.0 默认认证插件为 caching_sha2_password,旧客户端(如 Sequel Pro)不兼容

修复:修改用户认证插件为 mysql_native_password

1
2
3
4
5
6
7
8
9
10
11
12
# 修改 root 用户
kubectl exec -n mysql mysql-0 -- mysql -h 127.0.0.1 -u root -p'{password}' -e "
ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY '{password}';
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '{password}';
FLUSH PRIVILEGES;
"

# 修改 gitea 用户
kubectl exec -n mysql mysql-0 -- mysql -h 127.0.0.1 -u root -p'{password}' -e "
ALTER USER 'gitea'@'%' IDENTIFIED WITH mysql_native_password BY '{password}';
FLUSH PRIVILEGES;
"

验证

1
kubectl exec -n mysql mysql-0 -- mysql -h 127.0.0.1 -u root -p'{password}' -e "SELECT user, host, plugin FROM mysql.user WHERE user IN ('root', 'gitea');"

相关文件

  • 备份文件:/mnt/nfs/mysql-gitea-backup.sql
  • 备份文件:/mnt/nfs/mysql-data.tar.gz
  • StatefulSet 配置:/mnt/nfs/mysql-statefulset.yaml
  • Deployment 配置:/mnt/nfs/mysql-deployment.yaml
  • Secret:mysql-secret(命名空间:mysql)